<?php
/**
 * Created by sublime 3.
 * Auth: Inhere
 * Date: 14-9-28
 * Time: 10:35
 * Uesd: 主要功能是 hi
 */
namespace ulue\core\view;

use Ioc;
use Ulue,
    Debug;

use ulue\base\TheAppException;
use ulue\core\abstracts\AbstractCore;
use ulue\core\TheSystemException;
use ulue\libs\traits\TraitGetOption;

abstract class AbstractView extends AbstractCore
{
    static public $viewConsts = [];


    protected $actionName     = '';

    protected $theme          = '';
    protected $themePath      = '';

    /**
     */
    protected $simpleLayout   = true;

    /**
     * 1 | 2
     * 1 simple 简单布局
     *     即简单的将页面分为 header content footer 三部分
     *     自动加载当前模块视图层下 public的 'header.html' 'footer.html'，作为视图文件的头部、尾部
     * 2 normal
     *     正常的使用布局文件布局，划分页面区域更灵活
     */
    protected $layoutType    = 2;
    protected $layoutFile;      //正在渲染的布局文件
    protected $layout;
    protected $layoutSuffix   = '.php';      //布局文件后缀

    //布局时主体内容输出：1使用替换{_mainContent_} 0 输出 echo $_mainContent_
    // protected $outputType    = true;

    protected $viewFile;            //正在渲染的视图文件
    protected $viewSuffix    = '.html';      //视图文件后缀
    protected $viewVars      = []; //需要解析到视图页面的变量组
    protected $viewData;

    //
    protected $files = [];

    static public $headerTag = [
        'contentType'   => 'text/html',
        'charset'       => 'UTF-8',
        'title'         => '',
        ];

    const POINT = '.';

    const TPL_VIEW      = 'view';
    const TPL_LAYOUT    = 'layout';

    /**
     * @var $data \ulue\core\ioc\Container
     */
    // protected $data           = null;

    // use TraitGetOption;

    public function __construct(array $options=[])
    {
        parent::__construct($options);

        $this->validateTheme();

        $this->setViewConst($this->defaultConsts());
    }

    /*
    protected function theGetMode()
    {
        return 'strict';
    }*/

    /**
     * 分配变量到视图页面
     * @param  mixed $name
     * @return void
     */
    public function allot($name, $value=null)
    {
        Debug::trace('i start allot Variable data ...');

        if ( is_null($value) && is_array($name) ) {
            $this->viewVars = array_merge($this->viewVars, $name);//连续分配多次
        } else if ( is_string($name) ) {
            $this->viewVars[$name] = $value;
        } else {
           \Trigger::error('变量分配错误!');
        }

        Debug::trace('allot Variable data end',['allot Variable Name list:'=>array_keys($this->viewVars)]);
    }

    // 分配变量 allot() 别名
    public function assign($name, $value=null)
    {
        $this->allot($name,$value);
    }

    // 渲染加载显示视图文件之前的操作。。。用于被覆盖
    public function beforeRenderAll($viewFile)
    {
        return true;
    }

    /**
     * 渲染视图,会自动加载布局文件，显示加载视图文件并解析变量
     * @param string $viewFile 视图文件
     * @param array $data 视图分配变量数据
     * @throws TheSystemException
     * @throws \DRuntimeException
     * @return bool|mixed|string
     */
    public function renderAll($viewFile='', array $data=[])
    {
        /**
         * 当视图文件就是当前控制器下action同名文件，可不写视图文件参数 $viewFile，
         * 可直接将第一个参数传入视图数据(array)
         */
        if (is_array($viewFile)) {
            $data       = $viewFile;
            $viewFile   = '';
        }

        # @param2 $data(array) 是视图分配变量数据
        if ($data && !is_array($data)) {
            \Trigger::error('传入参数 @param2 $data (分配视图变量)必须是以 键=>值 对形式的数组！'.
                    '<br>如：分配用户信息$userInfo到视图页面 array("user"=>$userInfo)');
        }

        # 添加分配变量
        if (is_array($data) && count($data)>0) {
            $this->allot($data);
        }

        Debug::trace("i 渲染视图",'$viewFile:'.$viewFile);

        if ( ! $this->beforeRenderAll($viewFile) ) {
            throw new \DRuntimeException( get_class()."::beforeRender() returned the false!");
        }

        // 解析主内容
        $output = $this->internalRender($viewFile,$this->viewVars);

        $layoutFile=$this->getLayoutFile( $this->layout );

        // 渲染布局文件
        $output = $this->internalRender($layoutFile, array('_mainContent_'=>$output));

        $this->afterRenderAll($viewFile, $output);

        Debug::trace("渲染加载显示视图结束。");

        return $output;
    }

    //渲染加载显示视图文件之后的操作。。。用于被覆盖
    public function afterRenderAll($viewFile, &$output)
    {
        return true;
    }

    /**
     * 局部渲染，直接显示输出$viewFile内容到页面,没有布局，会解析变量
     * @param string $viewFile
     * @param array $data
     * @return bool|mixed|string
     */
    public function render( $view='', array $data = array() )
    {
        $this->layoutType = 2;

        /**
         * 当视图文件就是当前控制器下action同名文件，可不写视图文件参数 $view，
         * 可直接将第一个参数传入视图数据(array)
         */
        if (is_array($view)) {
            $data  = $view;
            $view  = '';
        }

        # @param2 $data(array) 是视图分配变量数据
        if ($data && !is_array($data)) {
            \Trigger::error('传入参数 @param2 $data (分配视图变量)必须是以 键=>值 对形式的数组！'.
                    '<br>如：分配用户信息$userInfo到视图页面 array("user"=>$userInfo)');
        }

        # 添加分配变量
        if (is_array($data) && count($data)>0) {
            $this->allot($data);
        }
                /**
         * 在布局文件中调用另一个布局文件时，若需要的文件在同一个目录
         * 可使用 [./] 开头来指明要加载当前目录中的文件
         */
        // if ( substr($view, 0, 2) === './') {
        //     $file = dirname($this->viewFile) . trim($viewFile, '.') . $this->layoutSuffix;

        //     if ( !is_file($file)) {
        //         throw new \DNotFoundException("没有找到需要的view文件：$file ");
        //     }
        // } else {}

        $output = $this->internalRender($view, $this->viewVars);

        return $output;
    }

    /**
     * 简单的输出内容没有布局，不会解析变量，用于直接展示内容或静态的页面
     * $content 1.要输出的内容
     * $content 2.为文件名时候也可，必须带后缀@example index.html news/index.html
     * @param string $content
     * @param int $type
     * @param array $param
     * @throws \ulue\core\TheSystemException
     * @internal param int $type
     * @return string
     */
    abstract public function showContent($content='', $type=1, $param=[]);

    /**
     * 解析视图模板文件内容 解析传入视图的变量
     * $MyLayout bool true 自动加载当前模块视图层下 public的 'header' 'footer'，作为视图文件的头部、尾部
     * @param $viewFile
     * @param null $data
     * @throws \DMissArgumentException
     * @return bool|mixed|string
     */
    public function internalRender($viewFile, $data=null)
    {
        if ( $data && !is_string($data) && !is_array($data) ) {
            throw new \DMissArgumentException("the second param only allow string or array!");
        }

        $viewFile = $this->getViewFile($viewFile);
        $content  = '';

        Debug::trace('i 准备解析视图文件',"@param \$viewFile = {$viewFile}");

        // 获取缓冲区的内容 并抽取分配的变量到 页面
        if (is_array($data)) {
            $content  = $this->getBufferOutputStream($viewFile, $data);
        }

        // 将视图文件中的自定义字符串常量名替换成真正的常量
        foreach($this->getViewConst() as $name=>$const) {
            $content = str_replace($name, $const, $content);
        }

        Debug::trace("视图文件{$viewFile}数据解析完成。");

        return $content;
    }

    /**
     * 加载视图文件，解析传入视图的变量
     * @param  string $viewFile
     * @param  array  $viewVars
     * @return string
     */
    public function getBufferOutputStream($viewFile, array $viewVars=[])
    {
        Debug::trace('i 准备开启缓冲区加载解析视图，获取视图数据。',"@param \$viewFile = {$viewFile}");

        #  打开输出控制缓冲
        ob_start();
        ob_implicit_flush(false);

        if ($viewVars && is_array($viewVars)) {
            @extract($viewVars, EXTR_PREFIX_SAME, 'data_');
        }

        require $viewFile;

        $content = ob_get_clean();

        return $content;
    }

    /**
     * 查找得到视图文件
     * 包含使用主题的请况
     * @param string $viewFile
     * @throws TheSystemException
     * @return mixed
     */
    public function getViewFile($viewFile='')
    {
        if (!$viewFile) {
            $viewFile = Ulue::$app->getController()->getActionName();
        }

        return $this->findTemplateFile($viewFile, self::TPL_VIEW, [
                    'suffix'      => $this->viewSuffix,
                    'basePath'    => COMM_PATH,
                    'defaultPath' => Ulue::$app->getModule()->getPath()
                ]);
    }

    //查找得到布局文件
    //@example main => layouts/main.php | other/admin =>layouts/other/admin.php
    public function getLayoutFile($layoutFile, array $options=[])
    {
        if (!$layoutFile) {
            throw new \DLogicException('使用了布局渲染页面，但没有设置布局样式文件');
        }

        $default = [
                   'suffix'      => $this->layoutSuffix,
                   'basePath'    => COMM_PATH,
                   'defaultPath' => COMM_PATH
               ];

        $layoutFile = $this->findTemplateFile($layoutFile, self::TPL_LAYOUT, array_merge($default, $options));

        return ($this->layoutFile = $layoutFile);
    }

    /**
     * findTemplateFile() 查找视图模板文件 view layout
     * @param  string $name 视图文件名称|路径
     * @param  string $type view layout
     * @param array $options
     *  required ['suffix'=>xx, 'basePath'=>'path', 'defaultPath'=>'path']
     *  optional $options['dirName']
     * @throws TheAppException
     * @throws \DInvalidArgumentException
     * @throws \DMissArgumentException
     * @return string
     */
    public function findTemplateFile($name, $type='view', array $options=[])
    {
        if (!$name || !is_string($name)) {
            throw new \DInvalidArgumentException("第一个参数为字符串类型且不能为空");
        }

        $type = trim(strtolower($type));

        if (!in_array($type, ['view', 'layout'])) {
            throw new \DInvalidArgumentException('第二个参数是无效的！only allow [ view,layout ]');
        }

        $check = ['suffix', 'basePath', 'defaultPath'];

        if ( $result = array_diff($check, array_keys($options)) ) {
            throw new \DMissArgumentException( sprintf("缺少必要的选项设置 %s", implode(',', $result)) );
        }

        $dirName     = empty($options['dirName']) ? $type.'s' : $options['dirName'];

        if ($type == self::TPL_VIEW) {
            $dirName = 'resources/' . $dirName;
        }

        // 默认存放目录路径
        $defaultPath = rtrim($options['defaultPath'], '/\\ ')."/$dirName/";
        $suffix      = empty($options['suffix'])   ? '.php' : $options['suffix'];
        $key         = md5($name.$defaultPath);

        if ( is_file($name) ) {
            return ($this->files[$key]=$name);
        } else if ( isset($this->files[$key]) ) {
            return $this->files[$key];

        // 使用别名 @module @run @src @web
        } else if ( $name{0}==='@' ) {
            $file = rtrim(Ulue::resolveAlias($name),'/').$suffix;

            if (!is_file($file)) {
                throw new TheAppException("{$type} 文件 <em>$file</em> 不存在！",'load');
            }

            return $file;
        }

        // 基础路径，若在默认路径没找到则会查找此路径是否有需要的文件
        $basePath = rtrim($options['basePath'], '/\\ ') . "/$dirName/";
        $controllerName = Ulue::$app->getController()->getName();
        $filename = $name.$suffix;

        /**
         * use theme; widget 不受主题影响; 不再支持跨主题调用
         * @var $themePath string The current theme path
         */
        if ( $path=$this->themePath ) {
            if ($type == self::TPL_VIEW) {
                $moduleName     = Ulue::$app->module->name;
                $path .= $moduleName . '/' . $controllerName . '/';
            }

            $file = $path.$dirName.'/'.$filename;

            if ( file_exists($file) ) {
                return $file;
            }
        }

        /**
         * not use theme or the theme dir not found need file, find in default dir
         */
        $file   = $defaultPath. ($type == self::TPL_VIEW ? $controllerName.'/' : ''). $filename;

        if ( is_file($file) ) {
            return $file;
        } else if ( $type == self::TPL_LAYOUT ) {
           \Trigger::error("布局文件 <em> {$file} </em> 不存在!");
        }

        /**
         * not use theme or the theme dir not found need file, find in base dir
         */
        $file = $basePath . $filename;

        if ( !is_file($file) ) {
           \Trigger::error("视图文件 <em> {$filename} </em> 不存在!");
        }

        return $file;
    }

    /**
     * validate Theme，get theme path
     */
    public function validateTheme()
    {
        if (!$this->theme) {
            return false;
        }

        if ( substr($this->theme, 0, 1) === '@' ) {
            $path = Ulue::resolveAlias($this->theme).'/';
        } else {
            $path = Ulue::getPath('theme').'/'.$this->theme .'/';
        }

        if( !is_dir($path)) {
            throw new \DNotFoundException(sprintf('Not found the theme [%s] dir [%s]', $this->theme, $path));
        }

        $this->themePath = $path;

        return true;
    }

    public function setTheme($theme)
    {
        $this->theme = (string) $theme;

        return $this;
    }

    /**
     * get theme name
     * @return mixed|string
     */
    public function getTheme()
    {
        return $this->theme;
    }

    /**
     * get layout file name
     * @return mixed|string
     */
    public function getLayout()
    {
        return $this->layout;
    }

    public function setLayout($layout)
    {
        $this->layout=(string) $layout;

        return $this;
    }

    /**
     * 解析视图模板文件中的自定义常量字符串名
     * @param  string $name
     * @return array [type]
     */
    public function getViewConst($name='')
    {
        if (!$name) {
            return self::$viewConsts;
        }

        return isset(self::$viewConsts[$name]) ? self::$viewConsts[$name] : null;
    }

    public function setViewConst($name, $value='')
    {
        if (is_array($name)) {
            self::$viewConsts = array_merge(self::$viewConsts,$name);
        }

        if (is_string($name) && is_string($value) && $name && $value)
        {
            self::$viewConsts[$name] = $value;
        }

        return $this;
    }

    private function defaultConsts()
    {
        $controller    = Ulue::$app->getController();
        $controllerUrl = $controller->getRoute();
        $actionUrl     = $controller->getAction()->getRoute();
        $selfUrl       = $actionUrl.'?'.Ulue::$app->get('router')->getParams(true);

        return [
            '{_HOST_}'       => _HOST_,   //http://localhost/
            '{_APP_}'        => _APP_ ,   //http://localhost/APP_NAME/
            '{_STATIC_}'     => _STATIC_, //http://localhost/APP_NAME/assets
            '{_INDEX_}'      => _INDEX_,  //http://localhost/APP_NAME/index.php

            //http://localhost/APP_NAME/admin
            '{_MODULE_}'     => Ulue::$app->getModule()->getRoute(),// _MODULE_,

            //http://localhost/APP_NAME/admin/index
            '{_CONTROLLER_}' => $controllerUrl, // _CONTROLLER_,

            //http://localhost/APP_NAME/admin/index/index
            '{_ACTION_}'     => $actionUrl, // _ACTION_,

            //http://localhost/APP_NAME/admin/index/index/id/9
            '{_SELF_}'       => $selfUrl, //_SELF_
        ];
    }

    /**
     * 得到解析视图文件后的内容
     * @return [type]
     */
    public function getViewData()
    {
        return $this->viewData;
    }

    // 设置视图内容数据
    public function setViewData($data)
    {
        $this->viewData = $data;

        return $this;
    }

    public function getVars()
    {
        return $this->viewVars;
    }

    public function getVar($name)
    {
        return isset($this->viewVars[$name]) ? $this->viewVars[$name]: null;
    }

    public function service($name)
    {
        return Ulue::$app->$name;
    }

    public function get($name)
    {
        return $this->service($name);
    }

    public function __get($name)
    {
        if ( isset($this->viewVars[$name]) ) {
            return $this->viewVars[$name];
        }

        return parent::__get($name);
    }

    /**
     * Gets the value of viewSuffix.
     *
     * @return mixed
     */
    public function getViewSuffix()
    {
        return $this->viewSuffix;
    }

    /**
     * Sets the value of viewSuffix.
     *
     * @param mixed $viewSuffix the view suffix
     *
     * @return self
     */
    public function setViewSuffix($viewSuffix)
    {
        if ($viewSuffix && is_string($viewSuffix)) {
            $this->viewSuffix = $viewSuffix;
        }

        return $this;
    }

    /**
     * Gets the value of layoutSuffix.
     *
     * @return mixed
     */
    public function getLayoutSuffix()
    {
        return $this->layoutSuffix;
    }

    /**
     * Sets the value of layoutSuffix.
     *
     * @param mixed $layoutSuffix the layout suffix
     *
     * @return self
     */
    public function setLayoutSuffix($layoutSuffix)
    {
        if ($layoutSuffix && is_string($layoutSuffix)) {
            $this->layoutSuffix = $layoutSuffix;
        }

        return $this;
    }
}